package com.googlecode.wmlynar.springconfgraph;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedHashSet;

import com.thoughtworks.qdox.JavaDocBuilder;
import com.thoughtworks.qdox.JavaDocBuilder.ErrorHandler;
import com.thoughtworks.qdox.model.Annotation;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaField;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.parser.ParseException;

public class GraphStringBuilder {
	
	public static boolean includeUtility = true;

	public static void main(String[] args) throws IOException {
		ArrayList<String> folders = new ArrayList<String>();
		folders.add("src/main/java");
		
		String output = buildArchitectureString(folders);
		
		PrintWriter out = new PrintWriter("target/confluence_architecture.txt");
		
		try {
			out.println(output);
		} finally {
			out.close();
		}
	}


	/**
	 * Builds string with architecture build out of all configuration classes
	 */
	static String buildArchitectureString(ArrayList<String> folders) {
		JavaDocBuilder builder = instantiateJavadocBuilder(folders);

		ArrayList<JavaClass> configurationClasses = findConfigurationClasses(builder);
		
		String output = "";
		
		output += buildLegendString();
		output += buildAllConfigObjectsString(builder, configurationClasses);
		
		return output;
	}


	/**
	 * Creates builder and scans all folders given as parameters.
	 */
	private static JavaDocBuilder instantiateJavadocBuilder(ArrayList<String> folders) {
		JavaDocBuilder builder = new JavaDocBuilder();
		builder.setErrorHandler(new ErrorHandler() {
			@Override
			public void handle(ParseException arg0) {
				System.err.println(arg0.getMessage());
			}
		});
		for(String folder : folders) {
			builder.addSourceTree(new File(folder));
		}
		return builder;
	}


	/**
	 * Returns string that is a legend of colors used for objects.
	 */
	private static String buildLegendString() {
		String legend = "h3. Legend\n\n" +
				"{graph-from-table:direction=TB|node=fontsize=10|edge=fontsize=8|}\n" +
				"| bean |  |  | fillcolor=\"lightyellow\" |\n" +
				"| controller |  |  | fillcolor=\"green\" |\n" +
				"| service |  |  | fillcolor=\"red\" |\n" +
				"| external |  |  | fillcolor=\"yellow\" |\n" +
				"| utility |  |  | fillcolor=\"cyan\" |\n" +
				"{graph-from-table}\n\n";
		
		if(!includeUtility) {
			legend += "Utility classes excluded for simplicity\n";
		}
		
		return legend;
	}


	/**
	 * Builds string with architecture for all configuration objects.
	 */
	private static String buildAllConfigObjectsString(JavaDocBuilder builder, ArrayList<JavaClass> configurationClasses) {
		String output = "";
		
		for(JavaClass c : configurationClasses) {
			output += "h3. " + c.getName() + "\n\n";
			output += buikdConfigObjectString(builder, c.getFullyQualifiedName());
			output += "\n";
		}
		
		return output;
	}

	/**
	 * externally referenced object should be colored in yellow
	 * because they will not have their own table row
	 * they will only be referenced by other objects (that will not change their colour)
     *
	 * then attributes should be colored cyan because they will be listed next
	 * (referencing object can not change their color)
     *
	 * then all other objects should be colored depending on their naming
	 *
	 * the name of the bean should be the same as variable of the bean or attribute
	 * it their name is the same as the type. if their name is different than type
	 * the type should be written below
	 */
	private static String buikdConfigObjectString(JavaDocBuilder builder, String configClassName) {
		String output = "{graph-from-table:direction=LR|node=fontsize=10,fillcolor=yellow|edge=fontsize=8|}\n";
		
		JavaClass configClass = builder.getClassByName(configClassName);
		
		
		ArrayList<JavaField> beanFields = getConfigAutowiredFields(configClass);
		ArrayList<JavaMethod> beanMethods = getConfigBeanMethods(configClass);
		
		output += buildFieldsString(beanFields);
		
		output += buildMethodObjectsString(beanMethods, beanFields);
		
		output += "{graph-from-table}\n";
		
		return output;
	}


	/**
	 * Build architecture string for all configuration methods annotated as bean.
	 */
	private static String buildMethodObjectsString(ArrayList<JavaMethod> configurationMethods,
			ArrayList<JavaField> configurationFields) {
		String output = "";
		for(JavaMethod method : configurationMethods) {
			String printedMethodName = getFullName(method);
			
			LinkedHashSet<String> connections = getObjectConnections(
					method, configurationMethods, configurationFields);

			// remove itself
			connections.remove(printedMethodName);
			
			String color = getColorForObjectName(method.getName());
			String colorParameter = "fillcolor=\"" + color + "\"";
			
			output += buildMethodConnectionsString(printedMethodName, connections,
					colorParameter);

		}
		return output;
	}


	/**
	 * if method does not have any parameters we need to add explicit object for it
	 * otherwise add connection to all other objects
	 */
	private static String buildMethodConnectionsString(String printedMethodName, LinkedHashSet<String> connections,
			String colorParameter) {
		String output = "";
		
		if(connections.isEmpty()) {
			output += "| " + printedMethodName + " |  |  | " + colorParameter + " |\n";
		} else {
			for(String connection : connections) {
				output += "| " + printedMethodName + " | " + connection + " | | " + colorParameter + " |\n";
			}
		}
		return output;
	}


	/**
	 * Returns all connections of particular object. Parameters, fields and other methods
	 * referenced in method body (if just by string comparison). Self can be part of the
	 * connection list as well;
	 */
	private static LinkedHashSet<String> getObjectConnections(
			JavaMethod method, ArrayList<JavaMethod> beanMethods,
			ArrayList<JavaField> beanFields) {
		LinkedHashSet<String> connections = new LinkedHashSet<String>();
		
		// add method parameters to connections
		for(JavaParameter parameter : method.getParameters()) {
			// if annotated @Value add value
			String value = getAnnotationValue(parameter);
			if(value!=null) {
				connections.add(parameter.getName() + "=" + escapeValueNameForConfluence(value));
			} else {
				connections.add(getFullName(parameter));
			}
		}
		// add local variables that exist in code block 
		String codeBlock = method.getCodeBlock();
		for(JavaField field: beanFields) {
			if(codeBlock.contains(field.getName())) {
				connections.add(getFullName(field));
			}
		}
		// add other beans that exist in code block 
		for(JavaMethod bean : beanMethods) {
			if(codeBlock.contains(bean.getName())) {
				connections.add(getFullName(bean));
			}
		}
		return connections;
	}

	/**
	 * Returns list of class fields that are annotated as autowired.
	 * The assumption is that those fields are beans.
	 */

	private static ArrayList<JavaField> getConfigAutowiredFields(
			JavaClass configClass) {
		ArrayList<JavaField> beanFields = new ArrayList<JavaField>(); 
		
		// can switch off including utility classes for clarity
		if(includeUtility) {
			for(JavaField attribute : configClass.getFields()) {
				if(isAutowired(attribute)) {
					beanFields.add(attribute);
				}
			}
		}
		return beanFields;
	}
	
	/**
	 * Build architecture string for all fields. Only fields marked as autowired
	 * shoulf be passed here.
	 */
	private static String buildFieldsString(ArrayList<JavaField> beanFields) {
		String output = "";
		for(JavaField field: beanFields) {
			output += "| " + getFullName(field) + " |  |  | fillcolor=\"cyan\" |\n";
		}
		return output;
	}


	/**
	 * Returns list of methods that are annotated as beans.
	 */
	private static ArrayList<JavaMethod> getConfigBeanMethods(
			JavaClass configClass) {
		ArrayList<JavaMethod> beanMethods = new ArrayList<JavaMethod>(); 
		for(JavaMethod method : configClass.getMethods()) {
			if(isBean(method)) {
				beanMethods.add(method);
			}
		}
		return beanMethods;
	}


	/**
	 * Make the first character of the method uppercase.
	 */
	private static String makeFirstLetterUppercase(String str) {
		return Character.toUpperCase(str.charAt(0)) + str.substring(1);
	}
	
	/**
	 * If object name is the same as class name it prints just object name else
	 * it creates correct name new line and class name.
	 */
	// TODO: instead getFullyQualifiedName() use getName and you do not have to remove package
	private static String getFullObjectName(String name, String type) {
		type = type.substring(type.lastIndexOf(".")+1);
		if(makeFirstLetterUppercase(name).equals(type)) {
			return name;
		}
		return name + " :\\l" + type;
	}
	

	private static String getFullName(JavaParameter parameter) {
		return getFullObjectName(parameter.getName(), parameter.getType().getFullyQualifiedName());
	}

	private static String getFullName(JavaMethod method) {
		return getFullObjectName(method.getName(), method.getReturns().getFullyQualifiedName());
	}
	
	private static String getFullName(JavaField field) {
		return getFullObjectName(field.getName(), field.getType().getFullyQualifiedName());
	}
	
	private static String getAnnotationValue(JavaParameter parameter) {
		for(Annotation a : parameter.getAnnotations()) {
			if("org.springframework.beans.factory.annotation.Value".equals(a.getType().getFullyQualifiedName())) {
				return a.getNamedParameter("value").toString();
			}
		}
		return null;
	}

	/**
	 * Escapes the name so that confluence can display it correctly.
	 */
	private static String escapeValueNameForConfluence(String string) {
		return string.replace("\"", "").replace("{", "\\{").replace("}", "\\}");
	}

	/**
	 * Returns proper color name depending on the type of object. Type of object
	 * is recognised by its last name.
	 */
	private static String getColorForObjectName(String name) {
		if(name.endsWith("Controller")) {
			return "green";
		} else if(name.endsWith("Service")) {
			return "red";
		}
		return "lightyellow";
	}

	/**
	 * Returns true if method is annotated as bean.
	 */
	private static boolean isBean(JavaMethod m) {
		for(Annotation a: m.getAnnotations()) {
			if("org.springframework.context.annotation.Bean".equals(a.getType().getFullyQualifiedName())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if field is marked autowired which can mean it is a bean.
	 */
	private static boolean isAutowired(JavaField m) {
		for(Annotation a: m.getAnnotations()) {
			if("org.springframework.beans.factory.annotation.Autowired".equals(a.getType().getFullyQualifiedName())) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Returns true if class is annotated as configuration.
	 */
	private static boolean isConfiguration(JavaClass c) {
		Annotation[] annotations = c.getAnnotations();
		for(Annotation annotation : annotations) {
			if("org.springframework.context.annotation.Configuration".equals(annotation.getType().getFullyQualifiedName())) {
					return true;
			}
		}
		return false;
	}

	/**
	 * Returns the list of all classes annotated as configuration.
	 */
	private static ArrayList<JavaClass> findConfigurationClasses(
			JavaDocBuilder builder) {
		ArrayList<JavaClass> configurationClasses = new ArrayList<JavaClass>(); 
		
		JavaClass[] classes = builder.getClasses();
		for(JavaClass c : classes) {
			if(isConfiguration(c)) {
				configurationClasses.add(c);
			}
		}
		return configurationClasses;
	}

}
